/**
 * Copyright 2009 Erick Armbrust 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0 
 *   
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */

package tapioca.util;

import java.util.ConcurrentModificationException;

import org.easymock.EasyMock;
import org.easymock.IMocksControl;

import tapioca.testing.AppEngineTestCase;
import tapioca.util.RetryHelper.TransactionalWorkUnit;
import tapioca.util.RetryHelper.WorkUnit;

import com.google.appengine.api.datastore.DatastoreFailureException;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreTimeoutException;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Transaction;

/**
 * Tests for the {@link RetryHelper} class.
 *
 * @author armbrust@gmail.com (Erick Armbrust)
 */
public class RetryHelperTest extends AppEngineTestCase {

  private final IMocksControl mockControl = EasyMock.createControl();
  private final DatastoreService mockDs = mockControl.createMock(DatastoreService.class);
  private final Transaction mockTxn = mockControl.createMock(Transaction.class);
  
  @Override 
  public void setUp() throws Exception {
    super.setUp();
    mockControl.reset();
  }

  @Override
  public void tearDown() throws Exception {
    mockControl.verify();
    super.tearDown();
  }  
  
  private void retryableWorkUnitFailureTest(RuntimeException toThrow) throws Exception {
    final Entity f1 = createAndSaveDtEntity("foo", "bar");

    // Fail once.
    EasyMock.expect(mockDs.get(f1.getKey())).andReturn(f1);
    EasyMock.expect(mockDs.put(f1)).andThrow(toThrow);
    
    // Then succeed.
    EasyMock.expect(mockDs.get(f1.getKey())).andReturn(f1);
    EasyMock.expect(mockDs.put(f1)).andReturn(f1.getKey());    
    
    mockControl.replay();
    
    RetryHelper helper = new RetryHelper(mockDs, new WorkUnit() {
      @Override
      public void doWork(DatastoreService ds) {
        try {
          ds.get(f1.getKey());
          ds.put(f1);
        } catch (EntityNotFoundException e) {
          fail("Unexpected EntityNotFoundException");
        }        
      }
    });
    helper.execute();
  }  
  
  private void retryableTransactionalWorkUnitFailureTest(RuntimeException toThrow) 
      throws Exception {
    final Entity f1 = createAndSaveDtEntity("foo", "bar");

    // Fail once.
    EasyMock.expect(mockDs.beginTransaction()).andReturn(mockTxn);
    EasyMock.expect(mockDs.get(mockTxn, f1.getKey())).andReturn(f1);
    EasyMock.expect(mockDs.put(mockTxn, f1)).andReturn(f1.getKey());
    EasyMock.expect(mockTxn.isActive()).andReturn(true);
    mockTxn.commit();
    EasyMock.expectLastCall().andThrow(toThrow);
    EasyMock.expect(mockTxn.isActive()).andReturn(false);
    
    // Then succeed.
    EasyMock.expect(mockDs.beginTransaction()).andReturn(mockTxn);
    EasyMock.expect(mockDs.get(mockTxn, f1.getKey())).andReturn(f1);
    EasyMock.expect(mockDs.put(mockTxn, f1)).andReturn(f1.getKey());
    EasyMock.expect(mockTxn.isActive()).andReturn(true);
    mockTxn.commit();
    EasyMock.expect(mockTxn.isActive()).andReturn(false);
    
    mockControl.replay();
    
    RetryHelper helper = new RetryHelper(mockDs, new TransactionalWorkUnit() {
      @Override
      public void doWork(DatastoreService ds, Transaction txn) {
        try {
          ds.get(txn, f1.getKey());
          ds.put(txn, f1);
        } catch (EntityNotFoundException e) {
          fail("Unexpected EntityNotFoundException");
        }        
      }
    });
    helper.execute();    
  }
  
  public void testExecuteWorkUnit_concurrentModificationException() throws Exception {
    retryableWorkUnitFailureTest(new ConcurrentModificationException("fail"));
  }
  
  public void testExecuteWorkUnit_datastoreTimeoutException() throws Exception {
    retryableWorkUnitFailureTest(new DatastoreTimeoutException("fail"));
  }
  
  public void testExecuteWorkUnit_datastoreFailureException() throws Exception {
    retryableWorkUnitFailureTest(new DatastoreFailureException("fail"));
  }
 
  public void testExecuteTransactionalWorkUnit_concurrentModificationException() throws Exception {
    retryableTransactionalWorkUnitFailureTest(new ConcurrentModificationException("fail")); 
  }
  
  public void testExecuteTransactionalWorkUnit_datastoreTimeoutException() throws Exception {
    retryableTransactionalWorkUnitFailureTest(new DatastoreTimeoutException("fail")); 
  } 
  
  public void testExecuteTransactionalWorkUnit_datastoreFailureException() throws Exception {
    retryableTransactionalWorkUnitFailureTest(new DatastoreFailureException("fail")); 
  }
  
  public void testExecuteWorkUnit_unrecognizedException() throws Exception {
    final Entity f1 = createAndSaveDtEntity("foo", "bar");
    RuntimeException fake = new RuntimeException("fake");

    // Fail once.
    EasyMock.expect(mockDs.get(f1.getKey())).andReturn(f1);
    EasyMock.expect(mockDs.put(f1)).andThrow(fake);
    
    mockControl.replay();
    
    RetryHelper helper = new RetryHelper(mockDs, new WorkUnit() {
      @Override
      public void doWork(DatastoreService ds) {
        try {
          ds.get(f1.getKey());
          ds.put(f1);
        } catch (EntityNotFoundException e) {
          fail("Unexpected EntityNotFoundException");
        }        
      }
    });
    
    try {
      helper.execute();
    } catch (RuntimeException e) {
      assertEquals(fake, e);
    }
  }
  
  public void testExecuteTransactionalWorkUnit_unrecognizedException() throws Exception {
    final Entity f1 = createAndSaveDtEntity("foo", "bar");
    RuntimeException fake = new RuntimeException("fake");

    // Fail once.
    EasyMock.expect(mockDs.beginTransaction()).andReturn(mockTxn);
    EasyMock.expect(mockDs.get(mockTxn, f1.getKey())).andReturn(f1);
    EasyMock.expect(mockDs.put(mockTxn, f1)).andReturn(f1.getKey());
    EasyMock.expect(mockTxn.isActive()).andReturn(true);
    mockTxn.commit();
    EasyMock.expectLastCall().andThrow(fake);
    EasyMock.expect(mockTxn.isActive()).andReturn(false);
    
    mockControl.replay();
    
    RetryHelper helper = new RetryHelper(mockDs, new TransactionalWorkUnit() {
      @Override
      public void doWork(DatastoreService ds, Transaction txn) {
        try {
          ds.get(txn, f1.getKey());
          ds.put(txn, f1);
        } catch (EntityNotFoundException e) {
          fail("Unexpected EntityNotFoundException");
        }        
      }
    });
    
    try {
      helper.execute();
    } catch (RuntimeException e) {
      assertEquals(fake, e);
    }
  }
  
}
